﻿/*                              
   Copyright 2011 Nils Kopal, Christopher Krüger, Simone Sauer, Dennis Nolte, Uni Duisburg-Essen

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Timers;
using System.Net;
using System.Collections;
using Sharebert.BootstrapServer.Properties;
using Sharebert.LinkLayerLibrary;
using Sharebert.LoggingLibrary;
using Sharebert.LoggingLibrary.API;

namespace Sharebert.BootstrapServer
{   
    public class SharebertBootstrapServer
    {
        private ILogger logger;
        private static ArrayList _peers = ArrayList.Synchronized(new ArrayList());
        private static object _lock = new object();

        /// <summary>
        /// Main entrance in our app
        /// </summary>
        /// <param name="args"></param>
        public static void Main(String[] args)
        {
            try
            {
                var server = new SharebertBootstrapServer();
                server.Start();
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.WriteLine(ex.StackTrace);
            }
        }

        public SharebertBootstrapServer()
        {
            this.logger = new Logbert();
            this.logger.LogDebug = false;
            logger.MessageLogged += new EventHandler<LogEventArgs>(HandleLogMessage);
        }

        /// <summary>
        /// Start the SharebertBootstrapServer
        /// </summary>
        private void Start()
        {
            Console.WriteLine("Welcome to Sharebert 2 Bootstrap Server\r\nTo stop this application please just close this window");
            Console.WriteLine("- Configured listen port is: " + Settings.Default.ListenPort);
            Console.WriteLine("- Configured check interval is: " + Settings.Default.CheckInterval);
            Console.WriteLine("- Configured delete interval is: " + Settings.Default.DeleteInterval);
            Console.WriteLine("Have fun...");

            var timer = new Timer();
            timer.Interval = Settings.Default.CheckInterval;
            timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
            timer.Start();

            var server = new BootstrapServer(IPAddress.Any, Settings.Default.ListenPort, logger);
            server.Timeout = Settings.Default.DeleteInterval;
            server.PeerListRequestReceived +=new EventHandler<PeerListRequestEventArgs>(server_PeerListRequestReceived);
            server.PeerIsOffline += new EventHandler<ClientEventArgs>(server_PeerIsOffline);
            server.PingReceived += new EventHandler<ClientEventArgs>(server_PingReceived);
            server.Start();
        }

        /// <summary>
        /// A peer wants a new list of other peers
        /// - we store him in our list, if we do not know him
        /// - we answer with a list of n peers (n is configurable)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void server_PeerListRequestReceived(object sender, PeerListRequestEventArgs args)
        {
            lock (_lock)
            {
                try
                {
                    var client = args.Client;
                    var query = from PeerEntry p in (ArrayList)_peers.Clone()
                                where p.IP.Equals(client.RemoteIP.ToString()) && p.Port.Equals(args.ListenPort)
                                select p;
                    
                    this.logger.Debug(String.Format("Received PeerListRequest from {0}:{1}",client.RemoteIP.ToString(),client.RemotePort));
                    
                    // we already know this peer so we refresh his timeout in our list:
                    if (query.Count() > 0)
                    {
                        foreach (PeerEntry peer in query)
                        {
                            peer.LastPingTime = DateTime.Now;
                            this.logger.Info(String.Format("Refreshed peer entry {0}:{1} | Active peers: {2}", client.RemoteIP.ToString(), args.ListenPort, _peers.Count));
                        }                        
                    }
                    //we do not know this peer so we add him to our list
                    else
                    {
                        var newEntry = new PeerEntry(client.RemoteIP.ToString(),args.ListenPort);
                        newEntry.LastPingTime = DateTime.Now;
                        _peers.Add(newEntry);
                        this.logger.Info(String.Format("Added peer entry {0}:{1} | Active peers: {2}", client.RemoteIP.ToString(), args.ListenPort, _peers.Count));
                    }
                    //finally we give some peers to the client
                    var peerEntries = new PeerEntry[Settings.Default.PeerCount];

                    //we have a less or equal size of peers the remote one needs,
                    //so we send them all (without remote peer himself)
                    if (_peers.Count <= Settings.Default.PeerCount)
                    {
                        int c = 0;
                        foreach (PeerEntry entry in _peers)
                        {
                            if (!entry.IP.Equals(client.RemoteIP.ToString()) || !entry.Port.Equals(args.ListenPort))
                            {
                                peerEntries[c] = entry;
                                c++;
                            }
                        }
                    }
                    else
                    {
                        //save randoms we already got 
                        var randoms = new List<int>();

                        for (int c = 0; c < Settings.Default.PeerCount; c++)
                        {
                            //randomly choose a number we do not know and a number that does
                            //not represent the remote peer
                            int random = -1;
                            do
                            {
                                random = RandomNumber(0, _peers.Count);
                            } while (randoms.Contains(random) ||
                                (((PeerEntry)_peers[random]).IP.Equals(client.RemoteIP.ToString()) &&
                                 ((PeerEntry)_peers[random]).Port.Equals(args.ListenPort)));
                            peerEntries[c] = (PeerEntry)_peers[random];
                            //save the random number because we already used it
                            randoms.Add(random);
                        }
                    }

                    var peerList = new PeerList(peerEntries, client.RemoteIP.ToString());
                    client.SendPeerList(peerList);
                    this.logger.Debug(String.Format("Sent PeerListResponse to {0}:{1}", client.RemoteIP.ToString(), client.RemotePort));
                    client.Disconnect();

                }
                catch (Exception ex)
                {
                    this.logger.Error("Exception during server_PeerListRequestReceived:", ex);
                }
            }
        }

        /// <summary>
        /// Helper method to generate a random number between min and max
        /// </summary>
        /// <param name="min"></param>
        /// <param name="max"></param>
        /// <returns></returns>
        private int RandomNumber(int min, int max)
        {
            var random = new Random();
            return random.Next(min, max);
        }

        /// <summary>
        /// A peer tells us that he is going down, so we remove him from our list
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void server_PeerIsOffline(object sender, ClientEventArgs args)
        {
            lock (_lock)
            {
                try
                {
                    var client = args.Client;
                    this.logger.Debug(String.Format("Received Offline from {0}:{1}", client.RemoteIP.ToString(), client.RemotePort));
                    var query = from PeerEntry p in (ArrayList)_peers.Clone()
                                where p.IP.Equals(client.RemoteIP.ToString()) && p.Port.Equals(args.ListenPort)
                                select p;
                    foreach (PeerEntry peer in query)
                    {
                        _peers.Remove(peer);
                        this.logger.Info(String.Format("Removed peer entry {0}:{1} (Offline) | Active peers: {2}", client.RemoteIP.ToString(), client.RemotePort, _peers.Count));
                    }
                    client.Disconnect();
                }
                catch (Exception ex)
                {
                    this.logger.Error("Exception during server_PeerIsOffline:", ex);
                }
            }
        }

        /// <summary>
        /// A peer pings us so we refresh him in our list
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void server_PingReceived(object sender, ClientEventArgs args)
        {
            lock (_lock)
            {
                try
                {
                    var client = args.Client;
                    this.logger.Debug(String.Format("Ping from {0}:{1} received.", client.RemoteIP.ToString(), args.ListenPort));

                    var query = from PeerEntry p in (ArrayList)_peers.Clone()
                                where p.IP.Equals(client.RemoteIP.ToString()) && p.Port.Equals(args.ListenPort)
                                select p;
                    if (query.Count() == 0)//maybe the peer was already deleted, so we add him again
                    {
                        var entry = new PeerEntry(client.RemoteIP.ToString(),args.ListenPort);
                        _peers.Add(entry);
                        this.logger.Info(String.Format("Added peer entry {0}:{1} | Active peers: {2}", client.RemoteIP.ToString(), args.ListenPort, _peers.Count));
                    }
                    else
                    {
                        foreach (PeerEntry peer in query)
                        {
                            peer.LastPingTime = DateTime.Now;
                            this.logger.Info(String.Format("Refreshed peer entry {0}:{1} | Active peers: {2}", client.RemoteIP.ToString(), args.ListenPort, _peers.Count));
                        }
                    }
                    client.Disconnect();
                }
                catch (Exception ex)
                {
                    this.logger.Error("Exception during server_PingReceived:", ex);
                }
            }
        }

        /// <summary>
        /// Check timer elapsed. So we check if a peers entry is too old and we have to remove him
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            lock (_lock)
            {
                try
                {
                    this.logger.Debug("Checking peer ages now");
                    var result = from PeerEntry peer in (ArrayList)_peers.Clone()
                                 where
                                     (peer.LastPingTime.AddMilliseconds(Settings.Default.DeleteInterval) <= DateTime.Now)
                                 select peer;
                    foreach (var peer in result)
                    {
                        _peers.Remove(peer);
                        this.logger.Info(String.Format("Removed peer entry {0}:{1} (Timed out) | Active peers: {2}", peer.IP, peer.Port, _peers.Count));
                    }
                }
                catch (Exception ex)
                {
                    this.logger.Error("Exception during timer_Elapsed!", ex);
                }
            }
        }

        private void HandleLogMessage(object sender, LogEventArgs e)
        {
            Console.WriteLine(String.Format("{0} {1}: {2}", DateTime.Now.ToLongTimeString(), e.Type, e.Message));
            if (e.Error != null)
            {
                Console.WriteLine(e.Error);
            }
        }
    }
}
